1 module server;
2 import handy_httpd;
3 import websocket_connection;
4 public import websocket_connection: pushWebsocketMessage;
5 import core.sync.semaphore;
6 import commons;
7 
8 enum DEFAULT_PATH="build";
9 private __gshared string startPath;
10 private __gshared ushort port = 9000;
11 
12 
13 int hipengineStartServer(string[] args, shared ushort* serverPort, shared string* serverHost, string servePath, shared Semaphore sem)
14 {
15     import std.stdio;
16 	import myip;
17 	import handy_httpd.handlers.path_handler;
18 	import core.thread;
19 	import std.socket;
20 
21 	import slf4d.default_provider;
22 	import slf4d;
23 
24 	auto provider = new DefaultProvider(false, Levels.ERROR);
25 
26 	string privateIp = "127.0.0.1";
27 	foreach(addr; privateAddresses)
28 	{
29 		if(isIpAddress(addr))
30 		{
31 			privateIp = addr;
32 			break;
33 		}
34 	}
35 	configureLoggingProvider(provider);
36 	ServerConfig cfg;
37 	cfg.hostname = "0.0.0.0";
38 	cfg.port = port;
39 	cfg.workerPoolSize = 3;
40 	cfg.enableWebSockets = true; // Important! Websockets won't work unless `enableWebSockets` is set to true!
41 	WebSocketHandler ws = new WebSocketHandler(new WebSocketReloadServer());
42 	PathHandler pathHandler = new PathHandler()
43 		.addMapping(Method.GET, "/ws", ws)
44 		.addMapping("/**", &serveGameFiles);
45 
46 
47 	*serverPort = port;
48 	*serverHost = privateIp.dup;
49 	startPath = servePath;
50 	writeln("HipremeEngine Dev Server listening from ",privateAddresses,":", port, " path ", startPath);
51 	(cast()sem).notify;
52 
53 	new HttpServer(pathHandler, cfg).start();
54 
55 	return 0;
56 }
57 
58 
59 private __gshared HttpRequestContext* context;
60 
61 void serveGameFiles(ref HttpRequestContext ctx)
62 {
63 	synchronized
64 	{
65 		if(context is null)
66 			context = &ctx;
67 	}
68     import std.path;
69     import std.stdio;
70 	import std.file;
71 	import std.string;
72 	import std.conv;
73 
74 	if(ctx.request.method != Method.GET)
75 		return;
76 
77 	string path = ctx.request.url.length > 0 ? ctx.request.url : "/";
78     string targetPath = buildNormalizedPath(startPath, DEFAULT_PATH);
79 	string contentType = contentTypeFromFileExtension(path);
80 	string file = buildNormalizedPath(targetPath, path[1..$]);
81 
82 
83 	string result;
84 
85 	if(path == "/")
86 	{
87 		string indexHTML = buildNormalizedPath(targetPath, "index.html");
88 		contentType = "text/html; charset=utf8";
89 		if(exists(indexHTML))
90 		{
91 			import std.string;
92 			string reloadServer = replace(import("reload_server.js"), "$WEBSOCKET_SERVER$", "ws://localhost:"~port.to!string~"/ws");
93 			result = readText(indexHTML)~"<script> "~reloadServer~"</script>";
94 		}
95 		else
96 		{
97 			// index
98 			string html = "<html><head><title>Hipreme Engine Webassembly Server</title></head><body><ul>";
99 			foreach(string name; dirEntries(targetPath, SpanMode.shallow))
100 			{
101 				name = name[targetPath.length..$];
102 				html ~= "<li><a href=\"" ~ name ~ "\">" ~ name ~"</a></li>";
103 			}
104 			html~= "</body></html>";
105 			result = html;
106 		}
107 	}
108 	else if(contentType)
109 	{
110 		if(!exists(file))
111 		{
112 			ctx.response.status = HttpStatus.NOT_FOUND;
113 			result = "404 file not found";
114 		}
115 		else
116 		{
117 			ctx.response.addHeader("Access-Control-Expose-Headers", "Content-Length");
118 			ctx.response.status = HttpStatus.OK;
119 			if(contentType.startsWith("text"))
120 				result = readText(file);
121 			else
122 				result = cast(string)read(file);
123 			writeln("GET ", file, " 200");
124 		}
125 	}
126 
127 
128 	ctx.response.writeBodyString(result, contentType);
129 
130 }
131 
132 string contentTypeFromFileExtension(string filename)
133 {
134 	import std.path;
135 	string ext = filename.extension;
136 	switch(ext)
137 	{
138 		case ".png":
139 			return "image/png";
140 		case ".apng":
141 			return "image/apng";
142 		case ".svg":
143 			return "image/svg+xml";
144 		case ".jpg":
145 			return "image/jpeg";
146 		case ".html":
147 			return "text/html";
148 		case ".css":
149 			return "text/css";
150 		case ".js":
151 			return "application/javascript";
152 		case ".wasm":
153 			return "application/wasm";
154 		case ".mp3":
155 			return "audio/mpeg";
156 		case ".pdf":
157 			return "application/pdf";
158 		case ".ogg":
159 			return "application/ogg";
160 		case ".mp4":
161 			return "application/mp4";
162 		default:
163 			return "application/octet-stream";
164 	}
165 }
166 
167 void stopServer()
168 {
169 	if(context is null)
170 		return;
171 	pushWebsocketMessage("close");
172 	context.server.stop();
173 }